﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace FitNesseRunner
{
	internal class ReflectionProvider : IReflectionProvider
	{
		private readonly IEnumerable<string> _imports;

		public ReflectionProvider(IEnumerable<string> imports)
		{
			_imports = imports;
		}

		private ConstructorInfo GetConstructor(string className, int parameterCount)
		{
			foreach (var import in _imports)
			{
				var type = FindType(import + "." + className);
				if (type == null)
					continue;

				return type.GetConstructors().Single(o => o.GetParameters().Count() == parameterCount);
			}

			throw new Exception("Failed to instantiate '" + className + "'");
		}

		public object CreateInstance(string className, IEnumerable<string> parameters)
		{
			var constructor = GetConstructor(className, parameters.Count());
			var constructorParameters = constructor.GetParameters();
			var values =
				from i in Enumerable.Range(0, constructorParameters.Length)
				select Convert.ChangeType(parameters.ElementAt(i), constructorParameters[i].ParameterType);

			var instance = constructor.Invoke(values.ToArray());
			return instance;
		}

		private static Type FindType(string fullTypename)
		{
			var appDomain = AppDomain.CurrentDomain;
			return (from assembly in appDomain.GetAssemblies()
			        let type = assembly.GetType(fullTypename)
			        where type != null
			        select assembly.GetType(fullTypename)).SingleOrDefault();
		}

		public object Invoke(object instance, string methodName, IEnumerable<string> parameters)
		{
			var method = GetMethod(instance.GetType(), methodName, parameters);
			var methodParameters = method.GetParameters();
			var values =
				from i in Enumerable.Range(0, methodParameters.Length)
				select Convert.ChangeType(parameters.ElementAt(i), methodParameters[i].ParameterType);

			var result = method.Invoke(instance, values.ToArray());
			return result;
		}

		private static MethodInfo GetMethod(Type type, string methodName, IEnumerable<string> parameters)
		{
			var parametersCount = parameters.Count();
			var methodInfo = GetMethod(type, methodName, parametersCount);

			if (methodInfo != null)
				return methodInfo;

			if (parametersCount > 0 && parameters.Last().Trim() == string.Empty)
				methodInfo = GetMethod(type, methodName, parametersCount - 1);

			if (methodInfo != null)
				return methodInfo;

			if (parametersCount > 0)
				throw new InvalidOperationException(string.Format("Method '{0}' with {1} parameters is not found", methodName, parametersCount));

			return type.GetProperty(methodName).GetGetMethod();
		}

		private static MethodInfo GetMethod(Type type, string methodName, int parametersCount)
		{
			return (from method in type.GetMethods()
			        let parameters = method.GetParameters()
			        where string.Equals(method.Name, methodName, StringComparison.OrdinalIgnoreCase) 
					&& parameters.Length == parametersCount
					&& parameters.All(p => p.ParameterType.Namespace == "System")
					select method).SingleOrDefault();
		}
	}
}